﻿using System;
using System.Xml;
using System.IO;
using System.Collections;
using System.Collections.Generic;

/// Version 1.0
namespace Core.Utils
{
    #region Event datatype/delegate
    /// <summary>
    /// Holds details about any errors that occured
    /// during the loading or parsing of the RSS feed.
    /// </summary>
    public class RssReaderErrorEventArgs : EventArgs
    {
        /// <summary>
        /// The details of the error.
        /// </summary>
        public string Message
        {
            get
            {
                return this.message;
            }
            set
            {
                this.message = value;
            }
        }

        private string message;
    }

    /// <summary>
    /// Represents the method that will handle the RssReader error event.
    /// </summary>
    public delegate void RssReaderErrorEventHandler(object sender, RssReaderErrorEventArgs e);
    #endregion

    #region RssReader class
    public class RssReader
    {
        // Events: XML document loaded, rss element found,
        // channel node found, item parsed, error

        /// <summary>
        /// This event is fired when the feed has finished loading from the URL
        /// provided, into the XML parser.
        /// </summary>
        public event EventHandler FeedLoaded;

        /// <summary>
        /// This event is fired when the root node (typically 'rss') has
        /// been found in the feed.
        /// </summary>
        public event EventHandler RssNodeFound;

        /// <summary>
        /// This event is fired when the channel/child node of the rss node
        /// (typically 'channel') has been found in the feed.
        /// </summary>
        public event EventHandler ChannelNodeFound;

        /// <summary>
        /// This event is fired when an item is added to the <see cref="RssFeed">RssFeed</see>'s
        /// collection of items.
        /// </summary>
        public event EventHandler ItemAdded;

        /// <summary>
        /// This event is fired when an error occurs in the loading or parsing
        /// of the feed. The same error message is also available in the ErrorMessage
        /// property of the <see cref="RssFeed">RssFeed</see> object that is returned
        /// by the <see cref="Retrieve">Retrieve</see> method.
        /// </summary>
        public event RssReaderErrorEventHandler Error;


        /// <summary>
        /// The node name for the channel element
        /// in the RSS feed. This will rarely ever to be
        /// changed. Default is 'channel'.
        /// </summary>
        public string RootNodeName
        {
            get
            {
                return this.rootNodeName;
            }
            set
            {
                this.rootNodeName = value;
            }
        }

        /// <summary>
        /// The node name for the root rss element
        /// in the RSS feed. This is altered automatically to 'rdf:RDF'
        /// when RdfMode is set to true. Default is 'rss'.
        /// </summary>
        public string ChannelNodeName
        {
            get
            {
                return this.channelNodeName;
            }
            set
            {
                this.channelNodeName = value;
            }
        }


        /// <summary>
        /// If this is set to true, then the XML document
        /// is parsed slightly different, to cater sites with RDF feeds (such as
        /// slashdot.org and register.com). The whole RDF format is not supported,
        /// but those items in RSS which have a corresponding RDF property, such
        /// as description,title for the channel, and title,description for each
        /// item, are matched.
        /// </summary>
        public bool RdfMode
        {
            get
            {
                return this.rdfMode;
            }
            set
            {
                if (value)
                {
                    this.rootNodeName = "rdf:RDF";
                }
                else
                {
                    this.rootNodeName = "rss";
                }
                this.rdfMode = value;
            }
        }

        /// <summary>
        /// Member for the public property.
        /// </summary>
        private string rootNodeName = "rss";

        /// <summary>
        /// Member for the public property.
        /// </summary>
        private string channelNodeName = "channel";

        /// <summary>
        /// Member for the public property.
        /// </summary>
        private bool rdfMode = false;

        /// <summary>
        /// Retrieves a <see cref="RssFeed">RssFeed</see> object using
        /// the url provided as the source of the Feed.
        /// </summary>
        /// <param name="Url">The url to retrieve the RSS feed from, this can
        /// be in the format of http:// and also file://.. (ftp?)</param>
        /// <param name="RdfFormat">If this is set to true, then the XML document
        /// is parsed slightly different, to cater sites with RDF feeds (such as
        /// slashdot.org and register.com). The whole RDF format is not supported,
        /// but those items in RSS which have a corresponding RDF property, such
        /// as description,title for the channel, and title,description for each
        /// item, are matched.</param>
        /// <returns>A <see cref="RssFeed">RssFeed</see> object from the
        /// RSS feed's details.</returns>
        public static RssFeed GetFeed(string Url, string Source, bool RdfFormat)
        {
            RssReader rssReader = new RssReader();
            rssReader.RdfMode = RdfFormat;
            return rssReader.Retrieve(Url, Source);
        }

        /// <summary>
        /// Retrieves a <see cref="RssFeed">RssFeed</see> object using
        /// the url provided as the source of the Feed.
        /// </summary>
        /// <param name="Url">The url to retrieve the RSS feed from, this can
        /// be in the format of http:// and also file://.. (ftp?)</param>
        /// <returns>A <see cref="RssFeed">RssFeed</see> object from the
        /// RSS feed's details.</returns>
        public static RssFeed GetFeed(string Url, string Source)
        {
            RssReader rssReader = new RssReader();
            return rssReader.Retrieve(Url, Source);
        }

        /// <summary>
        /// Retrieves an RSS feed using the given Url, parses it and
        /// creates and new <see cref="RssFeed">RssFeed</see> object with the information.
        /// If an error occurs in the XML loading of the document, or parsing of
        /// the RSS feed, the error is trapped and stored inside the RssFeed's
        /// ErrorMessage property.
        /// </summary>
        /// <param name="Url">The url to retrieve the RSS feed from, this can
        /// be in the format of http:// and also file://.. (ftp?)</param>
        /// <returns>An <see cref="RssFeed">RssFeed</see> object with information
        /// retrieved from the feed.</returns>
        public RssFeed Retrieve(string Url, string Source)
        {

            RssFeed rssFeed = new RssFeed();
            rssFeed.Items = new List<RssItem>();

            using (XmlTextReader xmlTextReader = new XmlTextReader(Url))
            {
                XmlDocument xmlDoc = new XmlDocument();

                try
                {
                    xmlDoc.Load(xmlTextReader);

                    // Fire the load event
                    if (this.FeedLoaded != null)
                    {
                        this.FeedLoaded(this, new EventArgs());
                    }

                    XmlNode rssXmlNode = null;

                    // Loop child nodes till we find the rss one
                    for (int i = 0; i < xmlDoc.ChildNodes.Count; i++)
                    {
                        if (xmlDoc.ChildNodes[i].Name == this.rootNodeName && xmlDoc.ChildNodes[i].ChildNodes.Count > 0)
                        {
                            rssXmlNode = xmlDoc.ChildNodes[i];

                            // Fire the found event
                            if (this.RssNodeFound != null)
                            {
                                this.RssNodeFound(this, new EventArgs());
                            }

                            break;
                        }
                    }

                    if (rssXmlNode != null)
                    {
                        XmlNode channelXmlNode = null;

                        // Loop through the rss node till we find the channel
                        for (int i = 0; i < rssXmlNode.ChildNodes.Count; i++)
                        {
                            System.Diagnostics.Debug.WriteLine("Rss child: " + rssXmlNode.ChildNodes[i].Name);
                            if (rssXmlNode.ChildNodes[i].Name == this.channelNodeName && rssXmlNode.ChildNodes[i].ChildNodes.Count > 0)
                            {
                                channelXmlNode = rssXmlNode.ChildNodes[i];

                                // Fire the found event
                                if (this.ChannelNodeFound != null)
                                {
                                    this.ChannelNodeFound(this, new EventArgs());
                                }

                                break;
                            }
                        }

                        // Found the channel node
                        if (channelXmlNode != null)
                        {
                            // Loop through its children, copying details to the
                            // RssFeed struct, and parsing the items
                            for (int i = 0; i < channelXmlNode.ChildNodes.Count; i++)
                            {
                                System.Diagnostics.Debug.WriteLine(channelXmlNode.ChildNodes[i].Name);
                                switch (channelXmlNode.ChildNodes[i].Name.ToLower())
                                {
                                    case "title":
                                        {
                                            rssFeed.Title = channelXmlNode.ChildNodes[i].InnerText;
                                            break;
                                        }
                                    case "description":
                                        {
                                            rssFeed.Description = channelXmlNode.ChildNodes[i].InnerText;
                                            break;
                                        }
                                    case "language":
                                        {
                                            rssFeed.Language = channelXmlNode.ChildNodes[i].InnerText;
                                            break;
                                        }
                                    case "copyright":
                                        {
                                            rssFeed.Copyright = channelXmlNode.ChildNodes[i].InnerText;
                                            break;
                                        }
                                    case "webmaster":
                                        {
                                            rssFeed.Webmaster = channelXmlNode.ChildNodes[i].InnerText;
                                            break;
                                        }
                                    case "pubDate":
                                        {
                                            rssFeed.PubDate = channelXmlNode.ChildNodes[i].InnerText;
                                            break;
                                        }
                                    case "lastBuildDate":
                                        {
                                            rssFeed.LastBuildDate = channelXmlNode.ChildNodes[i].InnerText;
                                            break;
                                        }
                                    case "category":
                                        {
                                            rssFeed.Category = channelXmlNode.ChildNodes[i].InnerText;
                                            break;
                                        }
                                    case "generator":
                                        {
                                            rssFeed.Generator = channelXmlNode.ChildNodes[i].InnerText;
                                            break;
                                        }
                                    case "ttl":
                                        {
                                            rssFeed.Ttl = channelXmlNode.ChildNodes[i].InnerText;
                                            break;
                                        }
                                    case "rating":
                                        {
                                            rssFeed.Rating = channelXmlNode.ChildNodes[i].InnerText;
                                            break;
                                        }
                                    case "skipHours":
                                        {
                                            rssFeed.Skiphours = channelXmlNode.ChildNodes[i].InnerText;
                                            break;
                                        }
                                    case "skipDays":
                                        {
                                            rssFeed.Skipdays = channelXmlNode.ChildNodes[i].InnerText;
                                            break;
                                        }
                                    case "managingEditor":
                                        {
                                            rssFeed.ManagingEditor = channelXmlNode.ChildNodes[i].InnerText;
                                            break;
                                        }
                                    case "item":
                                        {
                                            rssFeed.Items.Add(this.getRssItem(channelXmlNode.ChildNodes[i], Source));

                                            // Fire the found event
                                            if (this.ItemAdded != null)
                                            {
                                                this.ItemAdded(this, new EventArgs());
                                            }

                                            break;
                                        }
                                }

                            }

                            // If rdf mode is set, then the channel node only contains
                            // information about the channel, it doesn't hold the item
                            // nodes. The item nodes are children of the root node in
                            // an RDF document, so we use this instead.
                            if (this.RdfMode)
                            {
                                for (int i = 0; i < rssXmlNode.ChildNodes.Count; i++)
                                {
                                    switch (rssXmlNode.ChildNodes[i].Name)
                                    {
                                        case "item":
                                            {
                                                rssFeed.Items.Add(this.getRssItem(rssXmlNode.ChildNodes[i], Source));

                                                // Fire the found event
                                                if (this.ItemAdded != null)
                                                {
                                                    this.ItemAdded(this, new EventArgs());
                                                }

                                                break;
                                            }
                                    }
                                }
                            }
                        }
                        else
                        {
                            rssFeed.ErrorMessage = "Unable to find rss <seehannel> node";

                            // Fire the error event
                            if (this.Error != null)
                            {
                                RssReaderErrorEventArgs args = new RssReaderErrorEventArgs();
                                args.Message = rssFeed.ErrorMessage;
                                this.Error(this, args);
                            }
                        }

                    }
                    else
                    {
                        rssFeed.ErrorMessage = "Unable to find root <rss> node";

                        // Fire the error event
                        if (this.Error != null)
                        {
                            RssReaderErrorEventArgs args = new RssReaderErrorEventArgs();
                            args.Message = rssFeed.ErrorMessage;
                            this.Error(this, args);
                        }
                    }

                }
                catch (XmlException err)
                {
                    //
                    rssFeed.ErrorMessage = "Xml error: " + err.Message;

                    // Fire the error event
                    if (this.Error != null)
                    {
                        RssReaderErrorEventArgs args = new RssReaderErrorEventArgs();
                        args.Message = rssFeed.ErrorMessage;
                        this.Error(this, args);
                    }
                    return rssFeed;
                }
            }
            return rssFeed;
        }

        /// <summary>
        /// Creates an RSS item from an XML node with the 
        /// corresponding child nodes (title,description etc.)
        /// </summary>
        /// <param name="xmlNode">The node to extract the details from</param>
        /// <returns>An RssItem object with details taken from the item node.</returns>
        private RssItem getRssItem(XmlNode xmlNode, string Source)
        {
            RssItem rssItem = new RssItem();

            for (int i = 0; i < xmlNode.ChildNodes.Count; i++)
            {
                switch (xmlNode.ChildNodes[i].Name.ToLower())
                {
                    case "title":
                        {
                            rssItem.Title = xmlNode.ChildNodes[i].InnerText;
                            break;
                        }
                    case "description":
                        {
                            rssItem.Description = xmlNode.ChildNodes[i].InnerText;
                            break;
                        }
                    case "link":
                        {
                            rssItem.Link = xmlNode.ChildNodes[i].InnerText;
                            break;
                        }
                    case "author":
                        {
                            rssItem.Author = xmlNode.ChildNodes[i].InnerText;
                            break;
                        }
                    case "comments":
                        {
                            rssItem.Comments = xmlNode.ChildNodes[i].InnerText;
                            break;
                        }
                    case "pubdate":
                    case "dc:date":
                        {
                            rssItem.Pubdate = Convert.ToDateTime(xmlNode.ChildNodes[i].InnerText);
                            break;
                        }
                    case "guid":
                        {
                            rssItem.Guid = xmlNode.ChildNodes[i].InnerText;
                            break;
                        }
                }
            }

            rssItem.Source = Source;

            return rssItem;
        }
    }
    #endregion

    #region Data structures
    /// <summary>
    /// A data type to represent all properties of single RSS feed.
    /// (one XML document). The descriptions for
    /// the properties of RssItem are para-phrased from the 
    /// <see href="http://blogs.law.harvard.edu/tech/rss">RSS 2 specification</see>.
    /// See <see cref="RssReader">RssReader</see> for properties which 
    /// have not yet been implemented in this version of the
    /// the RssReader class.
    /// </summary>
    /// <remarks>
    /// The following elements of the RSS &lt;channel&gt; node aren't
    /// supported by this version of RssReader:
    /// <list type="bullet">
    /// <item>image (has subelements: image,url,title,link)</item>
    /// <item>cloud (has attributes: domain,port,path,registerProcedure,protocol)</item>
    /// <item>textInput (has subelements: title,description,name,link)</item>
    /// </list>
    /// </remarks>
    [Serializable()]
    public struct RssFeed
    {
        /// <summary>
        /// The name of the channel.
        /// </summary>
        public string Title;
        /// <summary>
        /// Phrase or sentence describing the channel.
        /// </summary>
        public string Description;
        /// <summary>
        /// The URL to the HTML website corresponding to the channel.
        /// </summary>
        public string Link;

        // Optional items

        /// <summary>
        /// The language the channel is written in. This allows 
        /// aggregators to group all Italian language sites, for example, on a single page. 
        /// </summary>
        public string Language;
        /// <summary>
        /// Copyright notice for content in the channel.
        /// </summary>
        public string Copyright;
        /// <summary>
        /// Email address for person responsible for technical issues relating to channel.
        /// </summary>
        public string Webmaster;
        /// <summary>
        /// The publication date for the content in the channel. 
        /// </summary>
        public string PubDate;
        /// <summary>
        /// The last time the content of the channel changed.
        /// </summary>
        public string LastBuildDate;
        /// <summary>
        /// Specify one or more categories that the channel belongs to.
        /// </summary>
        public string Category;
        /// <summary>
        /// A string indicating the program used to generate the channel.
        /// </summary>
        public string Generator;
        /// <summary>
        /// ttl stands for time to live. It's a number of minutes 
        /// that indicates how long a channel can be cached before 
        /// refreshing from the source
        /// </summary>
        public string Ttl;
        /// <summary>
        /// The <see href="http://www.w3.org/PICS/">PICS</see> rating for the channel.
        /// </summary>
        public string Rating;
        /// <summary>
        /// A hint for aggregators telling them which hours they can skip. 
        /// </summary>
        public string Skiphours;
        /// <summary>
        /// A hint for aggregators telling them which days they can skip. 
        /// </summary>
        public string Skipdays;
        /// <summary>
        /// Email address for person responsible for editorial content.
        /// </summary>
        public string ManagingEditor;
        /// <summary>
        /// A collection of RssItem datatypes, representing each
        /// item for the RSS feed.
        /// </summary>
        public List<RssItem> Items;
        /// <summary>
        /// Contains any errors that occured during the loading or
        /// parsing of the XML document. Compare this to a blank string
        /// to see if any errors occured.
        /// </summary>
        public string ErrorMessage;
    }

    /// <summary>
    /// A data type to represent a single
    /// RSS item in a RSS feed. See <see cref="RssReader">RssReader</see> for
    /// properties of a RSS item which have not yet been implemented 
    /// in this version of the the RssReader class. The descriptions for
    /// the properties of RssItem are para-phrased from the 
    /// <see href="http://blogs.law.harvard.edu/tech/rss">RSS 2 specification.</see>
    /// </summary>
    /// <remarks>
    /// The following elements of a RSS item aren't
    /// supported by this version of RssReader:
    /// <list type="bullet">
    /// <item>category (can have domain attribute)</item>
    /// <item>enclosure ( has attributes: url,length,type )</item>
    /// <item>source (has attributes: url)</item>
    /// </list>
    /// </remarks>
    [Serializable()]
    public struct RssItem
    {
        /// <summary>
        /// The title of the item.
        /// </summary>
        public string Title;
        /// <summary>
        /// The item synopsis.
        /// </summary>
        public string Description;
        /// <summary>
        /// The URL of the item.
        /// </summary>
        public string Link;
        /// <summary>
        /// Email address of the author of the item. 
        /// </summary>
        public string Author;
        /// <summary>
        /// URL of a page for comments relating to the item
        /// </summary>
        public string Comments;
        /// <summary>
        /// Indicates when the item was published. 
        /// </summary>
        public DateTime Pubdate;
        /// <summary>
        /// A string that uniquely identifies the item.
        /// </summary>
        public string Guid;
        /// <summary>
        /// A string store where comme from the data
        /// </summary>
        public string Source;
    }
    #endregion
}
